Skip to content

[FEAT] 웹소켓을 활용한 정보 공유 파이프라인 구현#221

Merged
coli-geonwoo merged 14 commits intodevelopfrom
feat/#220-websocket-setting
Nov 25, 2025
Merged

[FEAT] 웹소켓을 활용한 정보 공유 파이프라인 구현#221
coli-geonwoo merged 14 commits intodevelopfrom
feat/#220-websocket-setting

Conversation

@coli-geonwoo
Copy link
Contributor

@coli-geonwoo coli-geonwoo commented Nov 17, 2025

🚩 연관 이슈

closed #220

🗣️ 리뷰 요구사항 (선택)

내용이 깁니다... 노션에 3부로 나누어 정리해놓았으니 읽어주세요!
https://bustling-bathtub-b3a.notion.site/intro-2ae1550c60cf80768d90c4a6c4401b62?source=copy_link

Summary by CodeRabbit

  • 새로운 기능

    • WebSocket/STOMP 기반 실시간 이벤트 공유 기능 추가
    • 메시지 단위의 멤버 인증으로 인증된 사용자 식별 지원
    • 구독된 방으로의 발행 및 의장으로의 전달 흐름 도입
  • 설정 변경

    • CORS 설정을 구성 속성으로 전환해 다중 허용 출처 지원
  • 테스트

    • WebSocket/STOMP 연동 테스트와 테스트 헬퍼 유틸 추가
  • 오류 처리

    • 잘못된 roomId에 대한 전용 클라이언트 오류 코드 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@coli-geonwoo coli-geonwoo linked an issue Nov 17, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

Walkthrough

웹소켓 STOMP 엔드포인트(/ws), AuthMember 아규먼트 리졸버, 구독 이벤트 리스너·메시지 컨트롤러·관련 DTO 및 STOMP 테스트 인프라를 추가하여 실시간 공유 흐름을 구현합니다. (50자 이내)

Changes

Cohort / File(s) 변경 요약
의존성
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-websocket' 추가
웹소켓 구성 및 인증 리졸버
src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java, src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java
STOMP 엔드포인트(/ws)·브로커(/room, /chairman) 설정, @AuthMember 아규먼트 리졸버 등록 및 인증 토큰 추출/검증 로직 추가
컨트롤러
src/main/java/com/debatetimer/controller/sharing/SharingController.java
/app/event/{roomId} 메시지 매핑 핸들러 추가 — 수신된 SharingRequest.time을 포함한 SharingResponse/room/{roomId}로 발행
구독 이벤트 리스너
src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java
/room/{roomId} 구독 감지 시 ChairmanSharingRequest(roomId)/chairman/{roomId}로 발행; destination 파싱 및 유효성 검사 (INVALID_ROOM_ID 예외)
DTOs (요청/응답)
src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java, .../ChairmanSharingRequest.java, src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java
공유 관련 레코드 추가 (time, roomId)
CORS 설정 변경
src/main/java/com/debatetimer/config/CorsConfig.java, src/main/java/com/debatetimer/config/CorsProperties.java
CORS 설정을 CorsProperties 구성속성으로 전환, 입력 검증 추가 및 코드에서 사용 방식 변경
오류 코드
src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java
INVALID_ROOM_ID 오류 코드 추가
테스트 인프라
src/test/java/com/debatetimer/BaseStompTest.java, src/test/java/com/debatetimer/MessageFrameHandler.java
STOMP 테스트 베이스·메시지 핸들러 추가 (Jackson JavaTime 모듈 포함), 세션 연결/해제 관리
통합/유닛 테스트
src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java, src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java
SharingController 및 RoomSubscribeListener 동작 검증 테스트 추가
테스트 픽스처
src/test/java/com/debatetimer/fixture/HeaderGenerator.java
STOMP용 Authorization 헤더 생성 메서드(generateAccessTokenHeader) 추가
테스트 리네임 및 설정 변경
src/test/java/com/debatetimer/config/CorsPropertiesTest.java, src/test/resources/application.yml, src/test/java/com/debatetimer/controller/GlobalControllerTest.java, src/main/resources/application-*.yml
테스트 대상명을 CorsProperties로 변경 및 application.yml·dev·prod의 CORS 설정을 리스트 형태(cors-origin)로 구조 변경, 테스트 속성 키 조정

Sequence Diagram(s)

sequenceDiagram
    participant ClientA as 청중(Client)
    participant ClientB as 사회자(Client)
    participant Server as App (STOMP)
    participant RoomListener as RoomSubscribeListener
    participant SharingCtrl as SharingController

    Note right of Server `#eef6ff`: STOMP 엔드포인트 `/ws`, 브로커 `/room`·`/chairman`

    ClientA->>Server: /ws 연결 후 /room/{roomId} 구독\n(Authorization 헤더 포함)
    Server-->>RoomListener: SessionSubscribeEvent 전달
    RoomListener->>Server: /chairman/{roomId}에 ChairmanSharingRequest 발행

    ClientB->>Server: /app/event/{roomId}로 SharingRequest 전송
    Server->>SharingCtrl: `@MessageMapping` 처리 (WebSocketAuthMemberResolver로 AuthMember 주입)
    SharingCtrl->>Server: /room/{roomId}으로 SharingResponse 발행
    Server->>ClientA: SharingResponse 전달
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 집중 검토 지점:
    • WebSocketAuthMemberResolver: STOMP 헤더에서 토큰 추출, 예외 처리(UNAUTHORIZED_MEMBER) 및 authService 연동
    • RoomSubscribeListener: destination 파싱/유효성 검사 및 발행 대상 로직
    • CORS 구성 전환: CorsProperties 바인딩/검증 및 기존 사용처 영향
    • 테스트 인프라: BaseStompTest의 직렬화 설정, 비동기 연결 타임아웃과 테스트 동기화 안정성

Possibly related PRs

Suggested reviewers

  • leegwichan
  • unifolio0

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 WebSocket을 활용한 정보 공유 파이프라인 구현을 명확하게 설명하며, 변경사항의 핵심을 정확히 반영합니다.
Description check ✅ Passed PR 설명이 필수 섹션(연관 이슈, 리뷰 요구사항)을 포함하고 있으나, 구체적인 기술 설명이 부족하여 상세한 문서를 외부 링크(Notion)에만 의존합니다.
Linked Issues check ✅ Passed PR이 WebSocket 기본 세팅과 정보 공유 파이프라인 구현의 요구사항을 충족하며, 필요한 설정 및 컨트롤러가 모두 구현되어 있습니다.
Out of Scope Changes check ✅ Passed CorsConfig 및 CorsProperties 리팩토링은 WebSocket 설정과 직접 관련이 있으며, 모든 변경사항이 #220과 관련된 WebSocket 구현 범위 내입니다.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#220-websocket-setting

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a429cd and 0dc1af8.

📒 Files selected for processing (2)
  • src/main/resources/application-dev.yml (1 hunks)
  • src/main/resources/application-prod.yml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (2)
src/main/resources/application-dev.yml (1)

23-26: CORS 설정 구조 변경이 다른 환경 설정과 일관성이 있는지 확인이 필요합니다.

CORS 설정이 스칼라 값 형식에서 리스트 기반의 중첩 객체 구조로 변경되었습니다. AI 요약에 따르면 이 변경사항이 application.ymlapplication-prod.yml에도 적용되어야 한다고 언급되어 있습니다.

다음을 확인하십시오:

  1. application.ymlapplication-prod.yml에서 동일한 구조(lines 23-26)가 적용되었는지 확인
  2. 관련 CorsPropertiesCorsConfig 구현에서 cors-origin 리스트를 올바르게 처리하도록 업데이트되었는지 확인
src/main/resources/application-prod.yml (1)

22-25: CORS 설정 구조는 정확하며 문제 없음

검증 결과, CORS 설정 구조는 CorsProperties 클래스의 @ConfigurationProperties(prefix = "cors.origin") 바인딩과 완벽하게 일치합니다:

  • corsOrigin 필드(String[])는 YAML의 cors.origin.cors-origin[0] 경로와 올바르게 매핑됩니다
  • 모든 환경(dev, prod, test)에서 일관된 구조를 유지하고 있습니다
  • CorsConfigcorsProperties.getCorsOrigin()을 통해 배열을 정상적으로 사용합니다

중첩된 구조는 Spring Boot의 표준 @ConfigurationProperties 바인딩 패턴이며, 불필요한 중첩이 아닙니다.

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@unifolio0 unifolio0 added the feat 기능 추가 label Nov 17, 2025
@github-actions
Copy link

github-actions bot commented Nov 17, 2025

Test Results

124 files  124 suites   14s ⏱️
275 tests 275 ✅ 0 💤 0 ❌
287 runs  287 ✅ 0 💤 0 ❌

Results for commit 0dc1af8.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Nov 17, 2025

📝 Test Coverage Report

Overall Project 87.86% -0.29% 🍏
Files changed 93.62% 🍏

File Coverage
CorsConfig.java 100% 🍏
CorsProperties.java 100% 🍏
SharingController.java 100% 🍏
ClientErrorCode.java 100% 🍏
WebSocketConfig.java 100% 🍏
RoomSubscribeListener.java 84.78% -15.22% 🍏
WebSocketAuthMemberResolver.java 82.14% -17.86% 🍏

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/main/java/com/debatetimer/dto/sharing/request/ChairmanSharingRequest.java (1)

3-5: roomId 유효성 검증을 고려하세요.

현재는 문제가 없지만, 향후 유지보수성을 위해 @Positive 또는 @Min(1) 어노테이션을 추가하여 유효하지 않은 roomId가 전달되는 것을 방지하는 것을 고려해보세요.

 package com.debatetimer.dto.sharing.request;

+import jakarta.validation.constraints.Positive;
+
 public record ChairmanSharingRequest(
+        @Positive
         long roomId
 ) {

 }
src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java (1)

5-7: time 필드에 대한 유효성 검증을 고려하세요.

현재는 문제가 없지만, @NotNull 어노테이션을 추가하여 null 값이 전달되는 것을 방지하는 것을 고려해보세요.

 package com.debatetimer.dto.sharing.request;

+import jakarta.validation.constraints.NotNull;
 import java.time.LocalDateTime;

 public record SharingRequest(
+        @NotNull
         LocalDateTime time
 ) {

 }
src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java (1)

23-38: @AuthMember 파라미터 타입/토큰 포맷 계약 재확인 제안

구현 자체는 단순하고 명확한데, 두 가지 정도만 한 번 더 확인하면 좋을 것 같습니다.

  1. supportsParameter
    현재는 애노테이션만 체크하고 있어, 실수로 @AuthMember를 전혀 다른 타입 파라미터에 붙여도 통과합니다. 실제로 주입하려는 도메인 타입(예: Member)이 정해져 있다면, 타입까지 함께 검사하면 오용을 줄일 수 있습니다.

  2. Authorization 헤더 포맷
    getFirstNativeHeader(HttpHeaders.AUTHORIZATION)로 읽은 값을 그대로 resolveAccessToken(token)에 넘기고 있는데,

    • "Bearer <token>" 형태인지
    • 혹은 순수 토큰 문자열인지
      에 대한 계약이 AuthManager 쪽에 명확히 정의되어 있는지만 다시 한 번 점검 부탁드립니다. (빈 문자열 / 공백만 있는 값에 대한 처리도 함께요.)

크게 막히는 부분은 아니지만, 나중에 WebSocket 클라이언트가 늘어날 때 디버깅 비용을 줄이는 데 도움이 될 것 같습니다.

src/test/java/com/debatetimer/MessageFrameHandler.java (1)

17-21: handleFrame의 빈 if 블록 제거 및 타입 캐스팅 명확화 제안

CompletableFuture.complete(...)의 반환값을 사용하지 않으면서 빈 if 블록으로 감싸고 있어 의도가 혼동될 수 있습니다. 단순 호출로 바꾸고, tClass.cast(payload)를 사용하면 의도와 타입 캐스팅이 더 명확해집니다.

    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
-        if (completableFuture.complete((T) payload)) {
-        }
+        completableFuture.complete(tClass.cast(payload));
    }

테스트용 유틸이라도 이 정도 정리는 유지보수에 도움이 될 것 같습니다.

src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java (1)

24-35: WebSocket CORS 보안을 위해 allowedOriginPatterns 제한 고려

현재 STOMP 엔드포인트가 모든 Origin을 허용하도록 설정되어 있습니다.

registry.addEndpoint("/ws")
        .setAllowedOriginPatterns("*")
        .withSockJS();

개발 환경에서는 편하지만, 운영 환경에서는 불필요한 도메인에서의 WebSocket 접속을 모두 허용하게 되어 보안 측면에서 부담이 될 수 있습니다.

  • 운영용 프로파일에서는 구체적인 도메인만 허용하도록 설정하거나
  • application-*.yml에서 허용 Origin을 외부 설정으로 분리하는 방식

을 한 번 검토해 보시는 것을 추천드립니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 962e258 and 5481b31.

📒 Files selected for processing (13)
  • build.gradle (1 hunks)
  • src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java (1 hunks)
  • src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java (1 hunks)
  • src/main/java/com/debatetimer/controller/sharing/SharingController.java (1 hunks)
  • src/main/java/com/debatetimer/dto/sharing/request/ChairmanSharingRequest.java (1 hunks)
  • src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java (1 hunks)
  • src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java (1 hunks)
  • src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java (1 hunks)
  • src/test/java/com/debatetimer/BaseStompTest.java (1 hunks)
  • src/test/java/com/debatetimer/MessageFrameHandler.java (1 hunks)
  • src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java (1 hunks)
  • src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java (1 hunks)
  • src/test/java/com/debatetimer/fixture/HeaderGenerator.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java (1)
src/main/java/com/debatetimer/exception/custom/DTClientErrorException.java (1)
  • DTClientErrorException (5-10)
src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java (1)
src/test/java/com/debatetimer/MessageFrameHandler.java (1)
  • MessageFrameHandler (8-31)
src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java (1)
src/test/java/com/debatetimer/MessageFrameHandler.java (1)
  • MessageFrameHandler (8-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (6)
src/test/java/com/debatetimer/fixture/HeaderGenerator.java (1)

26-32: LGTM!

STOMP 헤더 생성을 위한 메서드 오버로딩이 적절하게 구현되었습니다. 목적지(destination)와 인증 토큰을 포함한 헤더 생성 로직이 올바릅니다.

src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java (1)

5-7: LGTM!

WebSocket 응답을 위한 간단하고 명확한 레코드 정의입니다.

src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java (1)

19-31: LGTM!

구독 리스너의 동작을 올바르게 검증하는 테스트입니다. /room/{roomId} 구독 시 /chairman/{roomId}로 메시지가 전달되는 것을 확인합니다.

src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java (1)

23-37: LGTM!

WebSocket 메시지 전송 및 수신 흐름을 올바르게 테스트합니다. 인증된 사용자의 이벤트 발생과 청중의 수신을 검증하는 로직이 적절합니다. 3초 타임아웃도 테스트 환경에 적합합니다.

build.gradle (1)

45-46: 검증 결과: WebSocket 의존성은 안전합니다.

Spring Boot 3.4.0 버전은 Maven Central에 존재하며 확인되었습니다. GitHub 보안 취약점 데이터베이스 조회 결과, org.springframework.boot:spring-boot-starter-websocket에 알려진 보안 취약점이 없습니다. 현재 코드는 문제가 없습니다.

src/main/java/com/debatetimer/controller/sharing/SharingController.java (1)

18-24: 현재 메서드에 권한 검증 로직이나 비즈니스 로직 추가 필요성 재확인 권장

코드 검증 결과, memberroomId 파라미터가 실제로 메서드 본문에서 사용되지 않음이 확인되었습니다. 다른 컨트롤러 메서드들과 비교해보면:

  • MemberController: membermember.getId() 또는 authService.logout(member, ...) 전달
  • PollController: memberpollService 메서드로 전달
  • CustomizeController: membercustomizeService 메서드로 전달

이들은 모두 파라미터를 서비스 계층에 전달하여 권한 검증이나 비즈니스 로직 처리에 사용합니다.

현재 SharingController.share() 메서드도 유사하게:

  1. member로 방 접근 권한 검증이 필요한지
  2. roomId로 공유 대상을 명시적으로 검증해야 하는지

재확인한 후, 향후 개발 계획이라면 TODO 코멘트를 추가하시기 바랍니다.

Comment on lines 22 to 33
@EventListener
public void handleSubscribeEvent(SessionSubscribeEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
String destination = accessor.getDestination();
if (destination == null) {
return;
}

if (destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX)) {
long roomId = Long.parseLong(destination.replace(AUDIENCE_SUBSCRIBE_PREFIX, ""));
messagingTemplate.convertAndSend(CHAIRMAN_CHANNEL_PREFIX + roomId, new ChairmanSharingRequest(roomId));
}
Copy link

@coderabbitai coderabbitai bot Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

구독 destination 파싱 시 잘못된 roomId에 대한 방어 코드 제안

현재 destination"/room/"로만 오거나, "/room/123/extra" 같이 예상하지 못한 값이 올 경우 Long.parseLong(...)에서 NumberFormatException이 발생해 리스너 전체가 실패할 수 있습니다.

예외를 로그로 남기고 무시하도록 방어 코드를 두면, 잘못된 구독 요청이 와도 다른 세션 처리에는 영향을 주지 않을 것 같습니다.

    @EventListener
    public void handleSubscribeEvent(SessionSubscribeEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        String destination = accessor.getDestination();
        if (destination == null) {
            return;
        }

        if (destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX)) {
-            long roomId = Long.parseLong(destination.replace(AUDIENCE_SUBSCRIBE_PREFIX, ""));
-            messagingTemplate.convertAndSend(CHAIRMAN_CHANNEL_PREFIX + roomId, new ChairmanSharingRequest(roomId));
+            String roomIdPart = destination.substring(AUDIENCE_SUBSCRIBE_PREFIX.length());
+            try {
+                long roomId = Long.parseLong(roomIdPart);
+                messagingTemplate.convertAndSend(
+                        CHAIRMAN_CHANNEL_PREFIX + roomId,
+                        new ChairmanSharingRequest(roomId)
+                );
+            } catch (NumberFormatException e) {
+                log.warn("Invalid room subscription destination: {}", destination, e);
+            }
        }
    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java around
lines 22 to 33, the code calls Long.parseLong on the destination substring
without validation which can throw NumberFormatException for bad values like
"/room/" or "/room/123/extra"; add defensive validation: after confirming
destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX) extract the suffix, validate
it contains only digits (or split on '/' and take the first segment) and parse
that; wrap parsing in a try/catch(NumberFormatException) and on error log a
warning including the invalid destination and exception, then return/ignore so
the listener continues handling other events.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coli-geonwoo 이 내용 한번 확인해주세요

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영해주었습니다... 값 객체로 감싸고 싶긴 했는데 너무 API 규약에 의존하는 느낌이라 순수한 VO 생성이 안될 것 같아 일단 private method로 처리해주었어요.

Comment on lines +62 to +75
@BeforeEach
public void connect() throws ExecutionException, InterruptedException, TimeoutException {
this.stompSession = this.websocketClient
.connectAsync(url + port + SOCKET_ENDPOINT, new StompSessionHandlerAdapter() {
})
.get(3, TimeUnit.SECONDS);
}

@AfterEach
public void disconnect() {
if (this.stompSession.isConnected()) {
this.stompSession.disconnect();
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

connect 실패 시 disconnect에서 NPE 발생 가능성

connect()에서 예외가 발생하면 stompSession이 초기화되지 않을 수 있는데, 이 경우 disconnect()

if (this.stompSession.isConnected()) {

에서 NullPointerException이 발생할 수 있습니다. 테스트 실패 원인을 흐리기도 해서, 널 체크를 추가하는 편이 안전해 보입니다.

    @AfterEach
    public void disconnect() {
-        if (this.stompSession.isConnected()) {
-            this.stompSession.disconnect();
-        }
+        if (this.stompSession != null && this.stompSession.isConnected()) {
+            this.stompSession.disconnect();
+        }
    }
🤖 Prompt for AI Agents
In src/test/java/com/debatetimer/BaseStompTest.java around lines 62 to 75, the
AfterEach disconnect() can throw a NullPointerException if connect() failed and
stompSession was never initialized; update disconnect() to first check that
this.stompSession is not null before calling isConnected() (e.g., guard with
this.stompSession != null && this.stompSession.isConnected()) and then call
disconnect() only when both conditions hold.

Copy link
Member

@leegwichan leegwichan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 고생 많으셨습니다 생각보다 겁나 빨리 했네요;;
어디 부분까지 구현되어 있고, 어디 부분까지 구현 안되어 있는지 적어주시면 더 좋을 것 같습니다!

Comment on lines +24 to +28
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/room", "/chairman");
registry.setApplicationDestinationPrefixes("/app");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 웹소켓 관련 로직이 추가되면, 해당 부분도 추가되야하는거죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다.

-> 메모리에 내장된 Spring이 제공해주는 SimpleBroker에 위임할 prefix는 enableSimpleBroker에 추가
-> MessageMapping을 통해 애플리케이션에서 우리가 처리할 로직은 setApplicationDestinations에 추가

Comment on lines +16 to +26
@Component
@RequiredArgsConstructor
public class WebSocketAuthMemberResolver implements HandlerMethodArgumentResolver {

private final AuthManager authManager;
private final AuthService authService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthMember.class);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 하면 웹소켓을 사용하지 않는 @AuthMember가 붙은 곳에도 해당 ArgumentResolver가 적용되는 건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

웹소켓 로직에만 적용됩니다.

웹소켓 MessageMapping 을 통해 애플리케이션에 위임된 메시지 핸들링 로직에서 @AuthMember가 붙은 엔드포인트에서 해당 로직을 인터셉트하여 실행해주어요.

package org.springframework.messaging.handler.invocation;

여기서 messaging이 Spring STOMP 소켓 관련 핸들링 로직이 있는 패키지입니다.

Comment on lines +16 to +24
@MessageMapping("/event/{roomId}")
@SendTo("/room/{roomId}")
public SharingResponse share(
@AuthMember Member member,
@DestinationVariable(value = "roomId") long roomId,
@Payload SharingRequest request
) {
return new SharingResponse(request.time());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시, 발행자-구독자가 모두 연결되어 있는 상황에서, 발행자가 갑자기 연결이 끊길 경우에 대한 처리가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

없습니다

그런데 이걸 백에서 해야할지도 조금 더 알아봐야할 것 같아요. 최근에 테코톡에서 웹소켓 끊김 대응전략을 보아서 오히려 프론트 단의 처리가 더 좋을 수도 있을 것 같아서요.

어떻게 클라이언트가 비정상종료되었는지를 서버가 감지하고 대응할지는 저도 공부가 더 필요할 것 같습니다.

Copy link
Contributor

@unifolio0 unifolio0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/noti
@coli-geonwoo 리뷰 완료했습니다! 아직 전체 기능이 완료된 건 아니기도 하고 파일 체인지 수도 적어서 크게 리뷰할 건 없었습니다

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 프론트와 협의가 안되서 그런 것 같지만 CorsConfig처럼 이 부분도 처리해주면 좋을 것 같아요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CorsProeperties로 CofiguranProperties로 객체 안에 CorsOrigin 검증을 캡슐화하고

이걸 WebSockerConfig 와 corsConfig 각각 의존성 주입받아 쓰도록 했습니다.

다만 바인딩 규칙에 따라 필드명에 매핑되는 ConfigurationProperties 특징으로 yml depth가 달라져서
dev, prod 배포 전에 secret 파일 업데이트가 필요합니다.
기존 : cors.origin
변경 후 : cors.origin.cors-origin

Comment on lines 22 to 33
@EventListener
public void handleSubscribeEvent(SessionSubscribeEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
String destination = accessor.getDestination();
if (destination == null) {
return;
}

if (destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX)) {
long roomId = Long.parseLong(destination.replace(AUDIENCE_SUBSCRIBE_PREFIX, ""));
messagingTemplate.convertAndSend(CHAIRMAN_CHANNEL_PREFIX + roomId, new ChairmanSharingRequest(roomId));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coli-geonwoo 이 내용 한번 확인해주세요

@@ -0,0 +1,7 @@
package com.debatetimer.dto.sharing.request;

public record ChairmanSharingRequest(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chairman이 뭔지 궁금해서 검색해보니 기업의 회장, 이사회의 의장 이런 개념이던데 맞을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사회자, 의장 으로 의도했습니다.

Copy link
Member

@leegwichan leegwichan Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(선택) 체어맨하면 한 눈에 이해하기 어려우니까, Announcer 나 구독자/발행자로 해보는 것도... (근데 애매하네요;;)
개인적으로는 체어맨하면 차 밖에 안떠올라서

Comment on lines +37 to +38
String email = authManager.resolveAccessToken(token);
return authService.getMember(email);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단순 궁금증입니다.

  1. AccessToken을 확인해보니 유효기간이 1시간으로 되어있던데 만약 중간에 토큰이 만료되면 다시 로그인해서 접속해야 되나요?
  2. getMember에 DB 조회 로직이 있어서 매 요청마다 등록된 모든 사람에 대한 DB 조회가 발생할 것 같은데 이 부분에 대한 의견이 궁금합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음... 재발급 과정은 비토가 말한대로 꽤 크리티컬 할 것 같아요. 1초 이상이 지연이 생기면 잘 연동되는 느낌이 아니라서요.

뭔가 프론트랑 규약할 때 웹소켓용으로 토큰을 재발급해주거나, 웹소켓 시에는 인증을 약하게 유지하는 방식도 있을 것 같아요. 확실한 건 기존 어플리케이션 인증용 토큰을 그대로 사용하면 안될 것 같습니다.

WebScoket Security 도 있어서 Spring 에서 제공해주는 웹소켓 보안 관련 스펙 한번 확인해보고 적절한 대안을 생각해볼게요. 이건 추후 따로 이슈파서 해결하겠습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/test/java/com/debatetimer/config/CorsPropertiesTest.java (1)

12-37: CorsProperties 검증 테스트 케이스 구조 적절합니다

null, 빈 배열, null/empty 요소에 대해 각각 다른 에러코드를 기대하도록 잘 분리돼 있어서, 프로퍼티 검증 로직 변경 시 회귀를 막는 데 도움이 될 것 같습니다.
여유가 된다면 정상 값(예: new String[] {"https://example.com"})에 대해 예외가 발생하지 않는 케이스도 하나 정도 추가해 두면, 설정 바인딩 변경 시 안심하고 리팩터링하기 더 좋을 것 같습니다.

src/main/java/com/debatetimer/config/CorsProperties.java (1)

1-31: CORS origin 검증 로직은 명확하지만, 불변성·바인딩 방식 한 번만 점검해 보시면 좋겠습니다

  • validate에서 null/빈 배열과 null·isBlank() 요소를 각각 다른 에러코드로 처리하는 부분은 의도가 잘 드러나고, 테스트와도 잘 맞습니다.
  • 다만 String[]를 그대로 필드에 보관하고 게터로 그대로 노출하면, 다른 빈에서 배열을 수정해 설정이 런타임에 변할 여지가 있습니다. 필요하다면 List<String>로 바꾸고 불변 리스트로 감싸거나, 생성자/게터에서 defensive copy를 만드는 쪽도 고려해 볼 만합니다.
  • 사용하는 Spring Boot 버전에 따라 @ConfigurationProperties + 생성자 하나/final 필드 조합이 constructor-binding으로 자동 지원되지 않을 수도 있어, 혹시 2.x 일부 버전이라면 @ConstructorBinding 추가가 필요한지 한 번만 확인 부탁드립니다.
src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java (1)

1-38: WebSocket/STOMP 설정이 공유 기능 요구사항에 잘 맞습니다

  • /room, /chairman를 simple broker 대상으로 두고 /app을 application destination prefix로 지정한 구성은 현재 공유 플로우(RoomSubscribeListener, SharingController 등)와 잘 정합됩니다.
  • /ws 엔드포인트에 corsProperties.getCorsOrigin()을 그대로 재사용해 HTTP CORS 설정과 WebSocket 핸드셰이크 origin 정책을 일원화한 것도 좋은 선택입니다.
  • 한 가지 정도만 제안하자면, "/room"·"/chairman" prefix가 이 클래스와 RoomSubscribeListener 등 여러 곳에 하드코딩되어 있으니, 공용 상수 클래스로 모으거나 enum 등으로 한 번만 정의해 두면 향후 경로 변경 시 실수를 줄이는 데 도움이 될 것 같습니다.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5481b31 and cebf6f5.

📒 Files selected for processing (7)
  • build.gradle (1 hunks)
  • src/main/java/com/debatetimer/config/CorsConfig.java (1 hunks)
  • src/main/java/com/debatetimer/config/CorsProperties.java (1 hunks)
  • src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java (1 hunks)
  • src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java (1 hunks)
  • src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java (1 hunks)
  • src/test/java/com/debatetimer/config/CorsPropertiesTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • build.gradle
🧰 Additional context used
🧬 Code graph analysis (4)
src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java (1)
src/main/java/com/debatetimer/exception/custom/DTClientErrorException.java (1)
  • DTClientErrorException (5-10)
src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java (1)
src/main/java/com/debatetimer/config/CorsConfig.java (1)
  • Configuration (11-34)
src/main/java/com/debatetimer/config/CorsProperties.java (1)
src/main/java/com/debatetimer/exception/custom/DTInitializationException.java (1)
  • DTInitializationException (5-10)
src/main/java/com/debatetimer/config/CorsConfig.java (1)
src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java (1)
  • Configuration (13-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (3)
src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java (1)

56-57: INVALID_ROOM_ID 에러 코드 추가가 올바르게 구현되었습니다.

검증 결과:

  • RoomSubscribeListener.java 라인 43에서 실제로 사용 중
  • ✅ 웹소켓 room ID 파싱 실패 시 적절하게 throw됨
  • ✅ HTTP 상태 코드와 에러 메시지 모두 일관성 있음
src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java (1)

1-46: 구독 이벤트 → 의장 채널 브로드캐스트 플로우가 명확하게 구현되어 있습니다

  • destination == null 인 경우 바로 리턴하고, /room/ prefix인 경우에만 처리하는 분기 구조가 STOMP 구독 이벤트용 리스너로 깔끔합니다.
  • parseRoomId에서 prefix 길이 이후 부분만 잘라 Long.parseLong으로 파싱한 뒤, 실패 시 INVALID_ROOM_ID로 감싸는 것도 WebSocket 구독 요청에 대한 도메인 에러 모델링 관점에서 일관성이 있습니다.
  • 이후 CHAIRMAN_CHANNEL_PREFIX + roomIdChairmanSharingRequest를 브로드캐스트하는 흐름도 WebSocketConfig의 브로커 설정(/room, /chairman)과 잘 맞아 보입니다.
src/main/java/com/debatetimer/config/CorsConfig.java (1)

3-22: CORS 설정을 CorsProperties로 일원화한 방향이 좋습니다

CorsConfigCorsProperties를 주입받아 allowedOriginPatterns(corsProperties.getCorsOrigin())를 사용하는 형태로 바뀌면서, HTTP CORS와 WebSocketConfig 양쪽 모두가 동일한 origin 설정을 공유하게 된 점이 깔끔합니다.
@EnableConfigurationProperties(CorsProperties.class)@RequiredArgsConstructor 조합도 설정 빈 구성 방식과 잘 맞습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cebf6f5 and 3a429cd.

📒 Files selected for processing (2)
  • src/test/java/com/debatetimer/controller/GlobalControllerTest.java (1 hunks)
  • src/test/resources/application.yml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (1)
src/test/java/com/debatetimer/controller/GlobalControllerTest.java (1)

11-11: 검증 결과: 프로퍼티 경로가 정확합니다.

src/test/resources/application.yml에서 확인한 결과, ${cors.origin.cors-origin[0]} 경로는 실제 구성과 정확히 일치합니다. YAML 구조가 cors > origin > cors-origin 순서로 중첩되어 있으며, [0]cors-origin 리스트의 첫 번째 요소를 올바르게 참조합니다.

개발/프로덕션 환경에서는 단순 문자열 값을 사용하지만, 테스트 환경에서는 리스트 기반 구조를 사용하는 유효한 패턴입니다. 원래 검토 의견에서 제기한 "중복적" 구조에 대한 우려는 근거가 없습니다.

Likely an incorrect or invalid review comment.

@coli-geonwoo
Copy link
Contributor Author

/noti

1차 리뷰 반영했습니다.

Copy link
Contributor

@unifolio0 unifolio0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/noti 보안쪽은 따로 이슈판다고 했기때문에 이만 approve합니다.

Copy link
Member

@leegwichan leegwichan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/noti @coli-geonwoo
동아리 활동 끝나고 이력서 넣고 이제 여유 생겨서 리뷰 남깁니다.\

  1. 각종 구현 내용이나 소개하는 것은 작업 내용 공유 페이지에 넣어놓는 것으로 하죠. (웹소켓 문서 나중에 찾기 힘듦)
  2. 앞으로는 어느 정도 구현되어 있는지 코드만을 보고 명확하게 파악하기 힘들다면, PR에 대략적으로 정리해서 작성 부탁드립니다. (ex. 구현 내용, 앞으로 해야 될 내용, ...)

@coli-geonwoo coli-geonwoo merged commit ca816b4 into develop Nov 25, 2025
3 of 4 checks passed
@coli-geonwoo coli-geonwoo deleted the feat/#220-websocket-setting branch November 25, 2025 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 웹소켓 기본 세팅

3 participants